iOS动手实现KVO

什么是KVO

KVO(key-value observing)是观察者模式的一种实现,通过监听对象的某一个属性,当属性的值发生改变的时候,监听者会获得通知。

KVO的使用以及缺点

我们来看一下是如何使用系统的KVO的。
系统提供了三个重要的方法,一个是设置监听,一个是监听回调,另外一个是移除监听。

1
2
3
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

怎么使用我就不说了,我来说说系统的KVO的缺点。

  1. 当一个观察者监听了很多对象的时候,所有的回调方法都在一个地方处理,这样就要做很多的判断来根据不同的对象不同的属性来做相应的响应。
  2. 没有提供@selector的形式以及block的形式,而且还要处理父类的情况,如果父类同样也监听了对象的同一属性,你不知道父类是否对这个消息感兴趣,还要根据context来处理,这样显得很混乱。

KVO的实现

基于以上的缺点考虑,我们想自己实现一个KVO,并且以block的形式进行回调,那么我们首先要知道KVO的实现原理。

在苹果的开发者网站上面有透露了KVO的实现,虽然没有透露内部的细节,但是我们也可以知道一个大概。

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

上面的话翻译过来就是KVO的实现是基于一个叫做isa-swizzling的技术,也就是改变isa的指针,我们知道每个对象都有一个isa指针指向他的类,后面调用方法的时候根据isa指针找到对应的类然后调用相应的方法。当一个对象被注册监听的时候,系统会创建一个该对象所属的类的子类,然后修改子类的setter方法,该setter方法里面会调用父类的setter实现,然后实现通知告诉观察者。最后改变该对象的isa指针,指向我们自定义的kvo类。最后当我们给一个属性赋值的时候,其实调用的setter方法应不是原来的类的setter,而是系统创建的kvo子类。

基于以上的原理,我们来开始动手实现一个通过block回调的kvo

实现带有block的KVO

我们创建一个NSObject的category,然后向外面提供两个方法:

1
2
3
4
5
6
7
8
9
10
typedef void(^lmObserveBlock)(id observer, NSString *key ,id oldValue, id newValue);

@interface NSObject (LMKVO)
- (void)lm_AddObserver:(id)observer
key:(NSString *)key
observeBlock:(lmObserveBlock)block;

- (void)lm_removeObserver:(id)observer
key:(NSString*)key;
@end

我们首先来看看添加监听的方法:

下面的代码主要做了下面四件事:

  1. 判断对象的类是否存在setter方法,不存在的话则直接返回
  2. 判断对象的isa指针指向的类是否是kvo类,不是的话就通过objc_allocateClassPair()以及objc_registerClassPair()两个主要方法来注册一个kvo类,修改对象的isa指针。
  3. 判断kvo类是否已经存在setter方法,不存在的话则创建setter方法.
  4. 通过obj_getAssociatedObject以及objc_setAssociatedObject来动态给分类增加一个array的属性,通过该属性保存所有的obverve信息,以便后面属性值发生改变的时候调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)lm_AddObserver:(id)observer
key:(NSString *)key
observeBlock:(lmObserveBlock)block{
SEL selForSetter = NSSelectorFromString([self setterName:key]);
Method setterMethod = class_getInstanceMethod([self class], selForSetter);
//lei判断如果父类不存在setter方法,那么直接返回
if (!setterMethod) {
NSLog(@"父类方法不存在setter");
return;
}
//判断对象的isa指针指向的类是否是kvo,如果不是则创建该类的子类,并且将对象的isa指向该子类。
Class class = object_getClass(self);
NSString *className = NSStringFromClass(class);

if (![className hasPrefix:LMKVOPREFIX]) {
//创建一个新的类并且修改isa指针
class = [self makeNewClassWithClassName:className];
object_setClass(self, class);
}

//判断新创建的类是否已经实现了setter方法,如果没有则创建新的setter方法
if (![self hasSelector:selForSetter]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(class, selForSetter, (IMP)kvo_setter, types);
}
//保存observerInfo
LMObserverInfo *info = [[LMObserverInfo alloc] initWithObserver:observer key:key block:block];
NSMutableArray *observers = objc_getAssociatedObject(self,(__bridge const void * _Nonnull)(LMKVOAssociatedObservers));
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(LMKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
if ([self containsSameObserver:observer key:key]) {
return;
}
[observers addObject:info];
}

然后我们来看一下移除监听的方法实现:

这里就是在我们之前保存的数组中找到对应的observe和key,然后移除相应的对象。

1
2
3
4
5
6
7
8
- (void)lm_removeObserver:(id)observer
key:(NSString*)key{
NSMutableArray *observers = objc_getAssociatedObject(self,(__bridge const void * _Nonnull)(LMKVOAssociatedObservers));
LMObserverInfo *specialInfo = [self containsSameObserver:observer key:key];
if (specialInfo) {
[observers removeObject:specialInfo];
}
}

实现的逻辑很简单,看看demo就一目了然了。
这里是[demo][https://github.com/codemonkeybulucck/LMKVOTool]

参考:ImplementKVO

-------评论系统采用disqus,如果看不到需要翻墙-------------